Skip to content

fix(agent): stop the grounding rule and compaction summaries from licensing fabricated answers - #2149

Merged
AlexLiu190625 merged 9 commits into
xorbitsai:mainfrom
AlexLiu190625:fix/grounding-placeholder-clause
Sep 8, 2026
Merged

fix(agent): stop the grounding rule and compaction summaries from licensing fabricated answers#2149
AlexLiu190625 merged 9 commits into
xorbitsai:mainfrom
AlexLiu190625:fix/grounding-placeholder-clause

Conversation

@AlexLiu190625

@AlexLiu190625 AlexLiu190625 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR rewrites three prompt texts used by the agent: the grounding rule shared by every pattern, the compaction summary prompt, and the two sibling sentences that follow the grounding rule call in the auto and dag patterns. No execution logic changes; every diff is inside a string literal or a docstring. The goal is to close two gaps that let a model present unsourced or fabricated content to a user as if it were real: a labelling loophole in the grounding rule, and a next-action instruction in the compaction summary prompt that neither summarizer nor caller could actually honor.

Behavior changes

  • grounding rule: unsourced values of any listed kind (numbers, person or organization names, identifiers, dates, statuses, table rows) must now be omitted and reported as missing, not filled in and labelled. The only exception is a current request that explicitly asks for a template or a sample, meaning content that is not meant to be real -- the qualifier binds the trigger rather than sitting beside it as a third alternative a request could satisfy on its own. The exception reaches the content wherever the request sends it: the answer, or document text the request asks the model to write and hand to a tool. Even then the model must state the content's nature in its reply before presenting the content, rather than appending a disclaimer afterward. Wording the model composes itself, such as a search query, code, or document text, stays exempt, but any fact written literally inside that composed text remains subject to the same sourcing rule.
  • compaction trailers: the two sentences appended after every compaction summary -- the lossy-paraphrase notice and the dropped-tool-results notice -- named a narrower set of value kinds than the grounding rule does ("figure" alone in the second one). Both now interpolate the same shared VALUE_KINDS constant, so a value kind added to the rule reaches them automatically. The first one keeps the "statistic" and "quotation" items it already carried, which the shared list does not have. These two sentences predate this PR (they landed in 5d53bef0, 2026-08-10) and were previously outside its diff; they are pulled in because they are the last prompt strings on this surface that stated a value-kind list of their own.
  • compaction summary prompt: no longer tells the summary what the next call should do with tools, since the summarizer cannot see what tools that call will have. Fact-carrying values for the records the current request points at must be copied character for character, never paraphrased or invented to complete a pattern. Credentials and unrelated personal information stay excluded even when they would otherwise qualify as one of those values. A dataset cannot be described as complete unless every item was both returned by a tool and is still present in the summary. The smallest output budget carries an explicit order for what to keep first, and the prompt itself is capped so it cannot silently outgrow it.
  • auto / dag sibling sentences: reworded to match the grounding rule's current wording. Auto's routing remedy now names the value kinds the rule forbids instead of the old "unsupported specifics" phrase. Dag's assessment sentence now talks about leaving out a missing value instead of labelling an illustrative placeholder.

Product decision disclosed

#1237 (proposal A of #1235) introduced a labelling clause for unsourced content and recorded "block outright vs allow with a label" as an open product decision. This PR resolves it in favor of blocking by default, with a single exception for a current user request that explicitly asks for a template or sample. Maintainers should weigh in on whether that is the right default.

Not in this PR

  • ordering of tool-set narrowing vs compaction in the ReAct loop
  • conditioning the compaction trailer's "re-read or re-query" instruction on whether the next call has tools (only the trailers' value-kind scope changed here)
  • structural credential scrubbing on the compaction path

Verification

  • Rebased onto 6f1f9ac2; head is 28292169.
  • Ran the five affected test files (test_auto, test_context, test_dag, test_grounding, test_react): the rebase base is 520 passed, this branch is 539 passed.
  • Ran fourteen independent mutations against the new prompt text; each one failed the named test and was then reverted:
    • swapped the second VALUE_KINDS reference for a hand-written five-item list missing one entry -> test_grounding_rule_keeps_literal_facts_inside_composed_text_sourced
    • removed the summary prompt's keep-in-this-order priority sentence -> test_compact_prompt_ranks_what_to_keep_when_the_budget_is_short
    • removed the "the exclusion wins: omit it" clause -> test_compact_prompt_excludes_credentials_even_when_also_an_identifier
    • reverted the dag sibling sentence to its old wording -> test_dag_completion_assessment_prompt_includes_grounding_rule
    • reverted the module docstring to its old wording -> test_grounding_module_docstring_states_the_default_as_a_prohibition
    • extended the summary prompt by ten words, crossing the word cap -> test_compact_prompt_does_not_grow_past_its_measured_ceiling
    • removed the sentence scoping the tool-argument exemption to the answer -> test_grounding_rule_scopes_the_answer_as_argument_exemption
    • narrowed "Never copy, in whole or in part" to "Never copy" -> test_compact_prompt_excludes_credentials_and_unrelated_personal_data
    • reverted the compaction trailer's value-kind list to its old wording -> test_compact_with_llm_summarizes_history_and_preserves_current_user
    • reverted the dropped-tool notice to naming only "figure" -> test_compact_with_llm_reports_dropped_tool_results_by_name
    • re-scoped the template exception to the answer alone -> test_grounding_rule_exception_requires_an_explicit_current_request and test_grounding_rule_exception_reaches_content_bound_for_a_tool_argument
    • restored "not meant to be real" as a third alternative in the trigger -> test_grounding_rule_exception_trigger_qualifies_what_a_sample_means
    • moved the value-kind list back after the relative clause -> test_grounding_rule_keeps_literal_facts_inside_composed_text_sourced
    • deleted the page-size/result-limit clause from tool_argument_rule -> test_grounding_rule_covers_fact_carrying_tool_arguments (before this round the same deletion left every test green)
  • pre-commit (ruff, mypy, isort, codespell) all green on the changed files.
  • The compaction summary system prompt is 327 words; a test pins a 330-word cap so it cannot grow past that without a deliberate change.

Part of #2146

@XprobeBot XprobeBot added the bug Something isn't working label Sep 5, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refines the LLM prompts for evidence compaction and grounding rules to strictly forbid unsourced values (such as names, dates, statuses, and identifiers) by default, requiring the model to report missing data as gaps rather than inventing values. It also updates the compaction prompt to prioritize verbatim preservation of requested records and adds comprehensive unit tests. The review feedback highlights an inconsistency in auto.py where the list of forbidden value kinds is narrower than the centralized VALUE_KINDS in grounding.py, and suggests aligning both the prompt and its corresponding test assertion to ensure consistent routing behavior.

Comment thread src/xagent/core/agent/pattern/auto/auto.py Outdated
Comment thread tests/core/agent/test_auto.py Outdated
@AlexLiu190625

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors and strengthens the grounding rules and LLM compaction prompts across the codebase to prevent data fabrication and hallucination (mitigating issue #1235). It introduces a centralized VALUE_KINDS definition to cover numbers, names, identifiers, dates, statuses, and table rows, and updates the grounding prompt to strictly forbid inventing these values by default, requiring gaps to be reported instead of using illustrative placeholders or caveats. The LLM compaction prompt is also updated to ensure verbatim preservation of requested records, forbid unearned completeness claims, and prioritize content when under a tight token budget. Corresponding updates are made to the Auto and DAG execution patterns, along with comprehensive test coverage validating these new behaviors. No review comments were provided, so there is no additional feedback.

@rogercloud rogercloud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execution.py:1245,1412 (outside this diff, see pointer on execution.py:1480) — [major] compaction trailer / dropped-tool notice still only names "figure", not the broader value-kind scope fixed elsewhere in this PR.
grounding.py:98-105 — [minor] template/sample exception scoped to "the answer" doesn't reach tool-argument content.
grounding.py:74 — [minor] tool_argument_rule keeps its own literal list instead of reusing VALUE_KINDS.
execution.py:1485 — [minor] "remaining work" phrasing risks being read as an implicit next-step instruction.
test_context.py:1252 — [minor] docstring miscites COMPACT_SUMMARY_MIN_TOKENS as basis for the 330-word input-prompt cap.
grounding.py:96 — [minor] relative clause reads as binding only to the last VALUE_KINDS list item.
grounding.py:99 — [minor] "a sample" trigger phrase is ambiguous between mockup content and a real example.
test_grounding.py:226 — [minor] ordering assertion checks two hardcoded substrings with no reorderable code path behind them.
grounding.py:17 / test_grounding.py:262 — [minor] denylist checks exact phrase "instructed default"; a synonymous rewrite evades it.
test_grounding.py:577 — [minor] coverage regression: no current test positively asserts the page-size/result-limit clause on the can_call_tools=True path.
test_dag.py:423 — [minor] assertion checks a phrase that never existed in dag.py's prompt; vacuously true.
test_context.py:1100 — [minor] _build_llm_compact_prompt_texts duplicates existing setup at ~1046-1055 instead of a shared helper.

Blocking: yes — recommended event: REQUEST_CHANGES
execution.py:1245,1412 — [new] compaction trailer/dropped-tool notice still only names "figure", missing the name/org/date/status types the incident actually fabricated

Comment thread src/xagent/core/agent/context/execution.py Outdated
Comment thread src/xagent/core/agent/grounding.py
Comment thread src/xagent/core/agent/grounding.py Outdated
Comment thread src/xagent/core/agent/context/execution.py Outdated
Comment thread tests/core/agent/test_context.py
Comment thread tests/core/agent/test_grounding.py Outdated
Comment thread tests/core/agent/test_grounding.py
Comment thread tests/core/agent/test_grounding.py
Comment thread tests/core/agent/test_dag.py
Comment thread tests/core/agent/test_context.py
The rule used to require an up-front disclaimer for unsourced figures
but otherwise let the model present them, and scoped that disclaimer
duty to figures and numbers specifically. A model could satisfy it by
inventing rows and appending a label copied near-verbatim from the
rule text.

Replace the disclosure duty with a default prohibition: unsourced
values of any enumerated kind (numbers, person or organization names,
identifiers, dates, statuses, table rows) must be omitted and reported
as missing. The only exception is a current user request that
explicitly asks for a template or sample, and even then the model must
state the nature of the content before presenting it rather than
appending a caveat afterward. The exemption for wording the model must
compose itself (search queries, code, document text) is unconditional
so all four call sites -- including the three forced-answer sites that
previously lacked any prompt covering this -- carry it, while facts
written literally inside such composed text remain subject to the
sourcing rule.

Also fix the module and function docstrings, which claimed disclosure
was the instructed default; that stopped being true once fabrication
without a source became a prohibition rather than a labelling
obligation.
Both patterns follow their grounding_rule() call with a local sentence
that named the old wording ("unsupported specifics", "illustrative
placeholder"). The rule no longer uses that vocabulary, so those
sentences pointed at wording the model would never see.

Auto's routing remedy is reworded to name the value kinds the rule
forbids without changing its own job (route to react so a tool can
supply the value). DAG's assessment sentence is reworded to describe
leaving out a value rather than labelling an illustrative placeholder,
matching the rule's gap-reporting instruction.
The compaction summary prompt asked the summarizer to "name the next
action needed" and told it the next call should continue "without
redoing completed tool calls." Both fabrication-incident summaries put
a do-not-call-tools instruction in that slot; the summarizer has no
visibility into what tools the next call will actually have.

Remove the next-action instruction entirely and state explicitly that
the summary must not instruct the next call on tool use. Require
fact-carrying values for the records the current request points at
(names, identifiers, statuses, dates, counts) to be copied character
for character rather than paraphrased or invented to complete a
pattern, with credentials and unrelated personal information excluded
even when they would otherwise qualify. Forbid claiming a dataset is
complete unless the history shows every item was both returned and is
still described in the summary -- the incident's dataset was fully
returned across nine tool calls, but four of those calls' raw payloads
had already been dropped by an earlier compaction. Give the smallest
output budget an explicit priority order for what survives, and cap
the prompt itself so it cannot silently outgrow the budget it exists
to fit inside.

The trailer appended after the summary is unchanged; it already
carries a correctly-conditioned "re-read or re-query the source"
instruction and is not part of this text.
…medy

The auto pattern's routing-remedy sentence and its test assertion each
hand-wrote a five-item value-kind list that had drifted from the
grounding module's six-item VALUE_KINDS constant (missing "a status").
Both now interpolate VALUE_KINDS directly, so the routing remedy always
matches the same value kinds the grounding rule itself forbids, with no
second literal copy left to fall out of sync.
@AlexLiu190625
AlexLiu190625 force-pushed the fix/grounding-placeholder-clause branch from cad2a02 to 2829216 Compare September 8, 2026 11:48

@rogercloud rogercloud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2: prompt-engineering fixes are correctly scoped but this remains pure prompt text with no code-level enforcement, and no test (here or in prior rounds) exercises the forced-final-answer x compaction crossing from #2146.

Minor

  • src/xagent/core/agent/context/execution.py:1467 — The lengthened prefix in _dropped_tool_results_notice shrinks the tool-name char budget (current_chars = len(prefix)) from ~746 to ~629 chars. Repro with 20 realistic MCP-style tool names (~30 chars) drops 3 extra names into "+N more" that used to fit. tests/core/agent/test_context.py:1455 only trips the 20-name cap with short names, never this budget path. Reserve headroom in the budget constant or add a longer-name test.
  • src/xagent/core/agent/pattern/auto/auto.py:1292 — "that no source here supports" reads as attached only to the last item of the VALUE_KINDS disjunction, not the whole list. grounding.py:99 fixed this exact ambiguity with different dash placement; match that pattern here.
  • src/xagent/core/agent/context/execution.py:1304 — "or report the value as unavailable" is grammatically scoped only to the write-tool branch; the no-read-tool-available case has no explicit fallback in this sentence (covered only implicitly by the general rule in grounding.py:58-61).
  • src/xagent/core/agent/grounding.py:25VALUE_KINDS is defined here but tool_argument_rule and execution.py:1517-1521's hand-written value-kind list don't reuse it, inviting future drift between lists (coverage itself is not currently broken).
  • src/xagent/core/agent/grounding.py:89 — The VALUE_KINDS splice reads as a closed enumeration at this and execution.py:1299,1467. Two of the four sites already hedge ("any value", "or other value"); add "including but not limited to" at the closed-looking ones for consistency.
  • src/xagent/core/agent/context/execution.py:1518 — The credential-exclusion clause ("the exclusion wins") only resolves identifier-vs-credential collisions, not handle-vs-credential (e.g. a URL that's both an artifact handle and carries embedded auth). Wording hardening only — no in-scope handle in this system carries embedded credentials today.
  • src/xagent/core/agent/context/execution.py:1534 — "Separate completed work from remaining work" sits next to "write no instruction about tool use," which is where both observed fabrication summaries put a directive. Author agrees this is a real failure shape but left the order as-is; the zero-cost reorder is still worth doing.
  • tests/core/agent/test_grounding.py:301test_grounding_module_docstring_states_the_default_as_a_prohibition denylists specific phrases plus a positive pin, but a docstring rewrite that keeps both required phrases while reintroducing "disclose instead of omit" in different words would pass undetected.
  • tests/core/agent/test_context.py:1111_build_llm_compact_prompt_texts duplicates setup already at test_context.py:1046-1055. Extracting just the five shared setup lines into a small parametrized helper would remove the duplication without touching either test's return-value contract.

Simplification

  • L159-212: delete: two near-identical banned-phrase-absence test functions (test_grounding_rule_offers_no_reusable_disclaimer_phrasing, test_grounding_rule_does_not_license_labelled_fabrication) with the same loop shape over the same two rule variants. Merge into one @pytest.mark.parametrize test over the combined phrase list; keep the one asymmetric "plausible-looking placeholder" check as a separate assertion.
    net: -12 lines possible

Blocking: no — recommended event: APPROVE

Comment thread src/xagent/core/agent/context/execution.py
Comment thread src/xagent/core/agent/pattern/auto/auto.py Outdated
Comment thread src/xagent/core/agent/context/execution.py
Comment thread src/xagent/core/agent/grounding.py
Comment thread src/xagent/core/agent/grounding.py
Comment thread src/xagent/core/agent/context/execution.py
The dropped-tool notice prefix spells out the shared value-kind list, and at 1024 characters that prefix crowded real tool names out of the very list of lost evidence the notice exists to give; 1152 holds a full page of names at the length an MCP server produces.

Punctuate auto's routing remedy so "that no source here supports" qualifies the value the answer would need rather than the last kind in the list, and tell the compaction trailer's reader to report a value as unavailable when no tool can supply it at all.

In the compaction prompt, the credential exclusion now covers a handle the request points at as well as an identifier, and the sentence forbidding instructions to the next call sits with the other rules about what the summary may say. Tests take the single-dropped-observation setup from one shared helper.
@AlexLiu190625
AlexLiu190625 added this pull request to the merge queue Sep 8, 2026
Merged via the queue into xorbitsai:main with commit 2ad8f64 Sep 8, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants